feat(identity): database overlay for federation identity (#326 Phase 1)#338
Merged
Conversation
Identity now resolves runtime override -> environment -> default. Nothing writes the identity.* rows yet, so every instance still resolves from the environment exactly as before; this is the read path and its safety properties, landed on its own so the write path has something proven to build on. The overlay lives in a new server-only identity-store.ts rather than in identity.ts, because site.config.ts imports identity.ts and that reaches client bundles — a prisma import there would pull the database client into the browser. The store pushes loaded values into the accessor instead, which keeps getIdentity() synchronous. That matters: roughly 60 call sites read it, several from sync helpers, and making it async would have meant converting all of them. Loaded once at boot from instrumentation.ts, awaited before the scheduler starts. Not per-request and not on a TTL: identity effectively never changes, a stale value here is far worse than a stale setting elsewhere, and a synchronous accessor cannot refresh itself anyway. A request served mid-load would sign with the environment's identity instead of the configured one — the same silent actor-id mismatch this whole area exists to prevent — so the load is awaited rather than fired off. Safety properties, all tested: a missing row falls through to the environment rather than to a default (the dangerous alternative is an instance quietly federating as @me@localhost:3000); junk values are rejected instead of building a malformed actor id; a database that is down or mid-migration leaves the instance on its environment identity rather than refusing to boot; and a failed reload drops a previously-loaded override rather than serving it blind. An override that IS in effect logs loudly, since "the identity isn't what .env.local says" is the first thing worth knowing when debugging federation. Also documents domain changes in docs/fediverse-setup.md. ActivityPub has no rename, and the alias + Move handshake only works while the OLD domain is still serving, because remote servers verify by fetching both ends. Lose the domain first and followers cannot be migrated by any later action. FediHome implements none of that handshake yet, so the honest guidance is to treat the domain as permanent, and the shape of a move that does work is written down. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Phase 1 of #326, on top of Phase 0 (#337). Read path only — nothing writes the
identity.*rows yet, so every instance still resolves from the environment exactly as before.What
Resolution becomes runtime override → environment → built-in default.
The overlay lives in a new server-only
identity-store.ts, not inidentity.ts. That's forced:site.config.tsimportsidentity.ts, andsite.config.tsreaches client bundles — aprismaimport there would pull the database client into the browser. The store pushes loaded values into the accessor instead.That design keeps
getIdentity()synchronous, which matters more than it looks: ~60 call sites read it, several from sync helpers, and making it async would have meant converting every one of them — churn on the federation-critical path for no benefit, since identity effectively never changes.Loaded once at boot, and awaited
instrumentation.tsawaitsloadIdentity()before the scheduler starts.Not per-request, and not on a TTL: a synchronous accessor can't refresh itself, and a stale identity is far worse than a stale setting elsewhere. A request served mid-load would sign with the environment's identity rather than the configured one — the same silent actor-id mismatch this area exists to prevent — so the load is awaited rather than fired and forgotten.
Safety properties (all tested)
@me@localhost:3000.env.localsays" is the first thing you'd want when debuggingactorId,keyId,webfingerSubject)Also: documented what a domain change really involves
docs/fediverse-setup.mdgains Changing Your Domain, and it's blunt because the constraint is:ActivityPub has no rename. The network's answer is an alias (
alsoKnownAs) on the new account plus aMovefrom the old — and remote servers verify it by fetching both ends. So the old domain has to still be serving while followers migrate. Lose the domain first and your followers cannot be brought across by any later action — you keep who you follow (that's your own database), but the people following you are gone.FediHome implements none of that handshake yet, so the honest guidance today is to treat the domain as permanent, plus the shape of a move that does work: new instance alongside the old, publish the move, leave the old server up for weeks, decommission last.
Verification
npx tsc --noEmitnpx eslint .npx vitest runnpm run buildBehaviour on any existing instance is unchanged — with no
identity.*rows,getIdentity()resolves precisely as it did before.Not in this PR
The write path, the admin UI, and the change-domain migration. Two constraints are recorded in the code for whoever builds them: the load is process-local (a pm2 cluster refreshes only the worker that handled the write — a partial rollout of a federation identity across workers is exactly the silent mismatch to avoid), and
Movesupport has to exist before a domain change can keep followers at all.🤖 Generated with Claude Code